﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BioPro1
{
    public class SekwencjeNarzedzia
    {
        private IDictionary<char, IDictionary<char, double>> podobienstwa;
        private ISet<char> alfabet;
        private const int STALA_DO_WZORU_KOSZT_PODOBIENSTWO = 1;

        public ISet<char> Alfabet
        {
            get
            {
                return alfabet;
            }
        }

        private struct Pole
        {
            public bool top;
            public bool left;
            public bool topleft;
            public double value;
        }

        public SekwencjeNarzedzia(IDictionary<char, IDictionary<char, double>> podobienstwa, ISet<char> alfabet)
        {
            this.podobienstwa = podobienstwa;
            this.alfabet = alfabet;
        }

        public String[] zlozDwaWielodopasowania(String[] wielodopasowanie1, String[] wielodopasowanie2)
        {
            var profil1 = zwrocProfil(wielodopasowanie1);
            var profil2 = zwrocProfil(wielodopasowanie2);

            /* Tworzona jest tabela najlepiej dopasowanych przedrostkow */
            Pole[,] tabela = new Pole[profil1.Length + 1, profil2.Length + 1];
            tabela[0, 0].value = 0;

            for(int i = 0; i <= profil1.Length; ++i)
            {
                for(int k = 1; k <= i; ++k)
                {
                    tabela[i, 0].value += (podobienstwoLiteryDoKolumny('-', profil1[k - 1]));
                }
            }
            for(int j = 0; j <= profil2.Length; ++j)
            {
                for(int k = 1; k <= j; ++k)
                {
                    tabela[0, j].value += (podobienstwoLiteryDoKolumny('-', profil2[k - 1]));
                }
            }
            for(int i = 1; i <= profil1.Length; ++i)
            {
                for(int j = 1; j <= profil2.Length; ++j)
                {
                    double max =
                        Math.Max(tabela[i - 1, j - 1].value + podobienstwoDwochKolumn(profil1[i - 1], profil2[j - 1]),
                                            tabela[i, j - 1].value + podobienstwoLiteryDoKolumny('-', profil2[j - 1]));
                    max = Math.Max(max, tabela[i - 1, j].value + podobienstwoLiteryDoKolumny('-', profil1[i - 1]));
                    tabela[i, j].value = max;
                }
            }
            /**************************************************************************/

            /* Wypelniane sa wskazniki poszczegolnych pol tabeli */
            for(int i = 1; i <= profil1.Length; ++i)
            {
                tabela[i, 0].left = true;
            }
            for(int j = 1; j <= profil2.Length; ++j)
            {
                tabela[0, j].top = true;
            }
            for(int i = 1; i <= profil1.Length; ++i)
            {
                for(int j = 1; j <= profil2.Length; ++j)
                {
                    double max = Math.Max(tabela[i - 1, j].value, tabela[i - 1, j - 1].value);
                    max = Math.Max(max, tabela[i, j - 1].value);

                    if(tabela[i - 1, j].value == max) tabela[i, j].left = true;
                    if(tabela[i - 1, j - 1].value == max) tabela[i, j].topleft = true;
                    if(tabela[i, j - 1].value == max) tabela[i, j].top = true;
                }
            }
            /******************************************************/

            /* Szukajac drogi od pola (n,m) do (0,0) zapisujemy ideksy kolumn */
            IList<int?> pozycjaKolumn1 = new List<int?>();
            IList<int?> pozycjaKolumn2 = new List<int?>();

            int n = profil1.Length;
            int m = profil2.Length;
            while(n > 0 || m > 0)
            {
                if(tabela[n, m].topleft)
                {
                    pozycjaKolumn1.Add(n);
                    pozycjaKolumn2.Add(m);
                    --n;
                    --m;
                }
                else if(tabela[n, m].left)
                {
                    pozycjaKolumn1.Add(n);
                    pozycjaKolumn2.Add(null);
                    --n;
                }
                else if(tabela[n, m].top)
                {
                    pozycjaKolumn1.Add(null);
                    pozycjaKolumn2.Add(m);
                    --m;
                }
                else
                {
                    throw new ApplicationException("Zaden wskaznik w polu tabeli dopasowan prefiksow nie zostal ustawiony. Wspolrzedne pola (" + n.ToString() + ", " + m.ToString() + ")");
                }
            }
            /*****************************************************/

            /* Na podstawie wyzej okreslonych pozycji kolumn skladamy pierwotne wielodopasowania */
            IEnumerable<int?> kolumny1 = pozycjaKolumn1.Reverse(); // kolejnosc byla zapisana od konca, wiec zostaje odwrocona
            IEnumerable<int?> kolumny2 = pozycjaKolumn2.Reverse();

            StringBuilder[] budowaneSekwencje = new StringBuilder[wielodopasowanie1.Length + wielodopasowanie2.Length];
            for(int i = 0; i < budowaneSekwencje.Length; ++i)
            {
                budowaneSekwencje[i] = new StringBuilder(pozycjaKolumn1.Count);
            }
            int index = 0;
            for(int i = 0; i < wielodopasowanie1.Length; ++i, ++index)
            {
                foreach(int? k in kolumny1)
                {
                    budowaneSekwencje[index].Append(k.HasValue ? wielodopasowanie1[i][k.Value - 1] : '-');
                }
            }
            for(int i = 0; i < wielodopasowanie2.Length; ++i, ++index)
            {
                foreach(int? k in kolumny2)
                {
                    budowaneSekwencje[index].Append(k.HasValue ? wielodopasowanie2[i][k.Value - 1] : '-');
                }
            }
            /*************************************************************************************/

            /* Konstruujemy ostateczny rezultat */
            String[] rezultat = new String[budowaneSekwencje.Length];
            for(int i = 0; i < rezultat.Length; ++i)
            {
                rezultat[i] = budowaneSekwencje[i].ToString();
            }
            /************************************/

            return rezultat;
        }

        private double podobienstwoLiteryDoKolumny(char litera, IDictionary<char, double> kolumna)
        {
            double s = 0;

            foreach(char c in alfabet)
            {
                s += (podobienstwa[litera][c] * (kolumna.ContainsKey(c) ? kolumna[c] : 0));
            }

            return s;
        }

        private double podobienstwoDwochKolumn(IDictionary<char, double> kolumna1, IDictionary<char, double> kolumna2)
        {
            double s = 0;

            foreach(char a in alfabet)
            {
                foreach(char b in alfabet)
                {
                    s += (podobienstwa[a][b] * (kolumna1.ContainsKey(a) ? kolumna1[a] : 0) * (kolumna2.ContainsKey(b) ? kolumna2[b] : 0));
                }
            }

            return s;
        }

        public IDictionary<char, double>[] zwrocProfil(String[] wielodopasowanie)
        {
            IDictionary<char, double>[] profil = new Dictionary<char, double>[wielodopasowanie[0].Length];

            for(int i = 0; i < wielodopasowanie[0].Length; ++i)
            {
                IDictionary<char, int> liczbaLiter = new Dictionary<char, int>();
                for(int j = 0; j < wielodopasowanie.Length; ++j)
                {
                    char c = wielodopasowanie[j][i];

                    if(!liczbaLiter.ContainsKey(c))
                    {
                        liczbaLiter[c] = 0;
                    }
                    liczbaLiter[c]++;
                }

                IDictionary<char, double> rozklad = new Dictionary<char, double>();
                foreach(char c in liczbaLiter.Keys)
                {
                    rozklad[c] = (double)liczbaLiter[c] / wielodopasowanie.Length;
                }

                profil[i] = rozklad;
            }

            return profil;
        }

        public double wartoscSP(String[] wielodopasowanie)
        {
            int licybaKolumn = wielodopasowanie[0].Length;
            double miaraSP = 0.0;
            for(int i = 0; i < licybaKolumn; i++)
            {
                for(int j = 0; j < wielodopasowanie.Length; j++)
                {
                    for(int k = j + 1; k < wielodopasowanie.Length; k++)
                    {
                        char firstLetter = wielodopasowanie[j][i];
                        char secondLetter = wielodopasowanie[k][i];
                        miaraSP += kosztZmiany(firstLetter, secondLetter);
                    }
                }
            }
            return miaraSP;
        }

        public double kosztZmiany(char a, char b)
        {
            if(a == b)
            {
                return 0;
            }
            else if(a == '-' || b == '-')
            {
                return 2;
            }
            else
            {
                return 1;
            }
        }

        public String[] sklejSekwencje(String[] sekwencje)
        {
            List<String[]> klastry = new List<String[]>();
            List<SklejenieKlatrow> sklejoneKlastry = new List<SklejenieKlatrow>();
            SklejenieKlatrow najtanszeSklejenie = null;

            foreach(String sekwencja in sekwencje)
            {
                klastry.Add(new String[] { sekwencja });
            }

            int numer_fazy = 1;
            while(klastry.Count > 1)
            {

                for(int i = 0; i < klastry.Count; i++)
                {
                    for(int j = i + 1; j < klastry.Count; j++)
                    {
                        SklejenieKlatrow sklejenieKlastrow = new SklejenieKlatrow();
                        sklejenieKlastrow.PierwszyKlaster = i;
                        sklejenieKlastrow.DrugiKlaster = j;
                        sklejenieKlastrow.Klaster = zlozDwaWielodopasowania(klastry.ElementAt(i), klastry.ElementAt(j));
                        sklejenieKlastrow.Sp = wartoscSP(sklejenieKlastrow.Klaster);

                        sklejoneKlastry.Add(sklejenieKlastrow);
                    }
                }

                Console.Write("\nNUMER FAZY: ");
                Console.WriteLine(numer_fazy++);
                Console.WriteLine("\n\n");

                for(int i = 0; i < sklejoneKlastry.Count; i++)
                {
                    for(int j = 0; j < sklejoneKlastry[i].Klaster.Length; j++)
                    {
                        Console.WriteLine(sklejoneKlastry[i].Klaster[j]);
                    }
                    Console.Write("\nWARTOŚ SP: ");
                    Console.WriteLine(sklejoneKlastry[i].Sp);
                    Console.WriteLine("\n\n");
                }

                Console.WriteLine("************************************************************");

                najtanszeSklejenie = null;

                foreach(SklejenieKlatrow sklejenie in sklejoneKlastry)
                {
                    if(najtanszeSklejenie == null)
                        najtanszeSklejenie = sklejenie;

                    if(najtanszeSklejenie.Sp > sklejenie.Sp)
                        najtanszeSklejenie = sklejenie;
                }

                klastry.RemoveAt(najtanszeSklejenie.PierwszyKlaster);
                klastry.RemoveAt(najtanszeSklejenie.DrugiKlaster - 1);

                klastry.Add(najtanszeSklejenie.Klaster);

                sklejoneKlastry.Clear();
            }

            return klastry.ElementAt(0);
        }

        public String zwrocSlowoKonsensusowe(String[] wielodopasowanie)
        {
            double najmniejszyKoszt = double.MaxValue;
            int indeksSlowaKonsensusowego = -1;
            for(int i = 0; i < wielodopasowanie.Length; i++)
            {
                double kosztSlowa = 0;
                kosztSlowa = kosztKonsensowy(wielodopasowanie[i], wielodopasowanie);
                if(kosztSlowa < najmniejszyKoszt)
                {
                    indeksSlowaKonsensusowego = i;
                    najmniejszyKoszt = kosztSlowa;
                }
            }
            return wielodopasowanie[indeksSlowaKonsensusowego].Replace("-", "");
        }

        public double kosztKonsensowy(String slowo, String[] wielodopasowanie)
        {
            double kosztKonsensowy = 0;
            foreach(String s in wielodopasowanie)
            {
                kosztKonsensowy += kosztDopasowania(slowo, s);
            }
            return kosztKonsensowy;
        }

        public double kosztDopasowania(String slowoA, String slowoB)
        {
            double koszt = 0;
            for(int i = 0; i < slowoA.Length; i++)
            {
                koszt += kosztZmiany(slowoA[i], slowoB[i]);
            }
            return koszt;
        }
    }
}
